<?php
/**
 * Module basic methods implementation.
 *
 * @author Martin "DevelX" Jurča
 */
abstract class Module {
    protected $name = 'Module';
    protected $administrable = false;
    protected $version = '2010-08-19';
    protected $details = array('dependencies' => array(),
            'observes' => array());
    protected $compatible = '0000-01-01';
    protected $system = false;
    protected $url = null;
    protected $rights = array();
    protected $rightLocales = null;
    protected $langArray;
    /**
     *
     * @var DBDriver 
     */
    protected $dbdriver;
    /**
     *
     * @var DBAPI
     */
    protected $dbapi;
    /**
     *
     * @var RecordList
     */
    protected $recordList;

    const MSG_CONFIRM = 0;
    const MSG_NOTICE = 1;
    const MSG_WARNING = 2;
    const MSG_ERROR = 3;

    public function __construct(DBAPI $dbapi, RecordList $recordList) {
        $this->dbapi = $dbapi;
        $this->dbdriver = $dbapi->getDriver();
        $this->recordList = $recordList;
        $langCode = Kernel::getLanguage();
        $lang = array();
        $localization = $this->getDetails();
        $localization = array_fill_keys($localization['localization'], true);
        $localization = $this->chooseLocale($localization);
        $this->loadLang($localization);
    }

    public function getName() {
        return $this->name;
    }

    public function getUIName() {
        return $this->lang('moduleName');
    }

    public function getDescription() {
        return $this->lang('moduleDescription');
    }

    public function getDetails() {
        if (!isset($this->details['localization']) ||
                (count($this->details['localization']) == 0)) {
            $loc1 = glob($this->getDir('/lang/??.php'));
            $loc1 = $loc1 ? $loc1 : array();
            $loc2 = glob($this->getDir('/lang/??_??.php'));
            $loc2 = $loc2 ? $loc2 : array();
            $this->details['localization'] = array_merge($loc1, $loc2);
            foreach($this->details['localization'] as &$locale) {
                $locale = substr($locale, strrpos($locale, '/') + 1);
                $locale = substr($locale, 0, strpos($locale, '.'));
            }
        }
        return $this->details;
    }

    public function getUrls() {
        if (!is_array($this->url) || (count($this->url) == 0)) {
            $loc1 = glob($this->getDir('/lang/url-??.php'));
            $loc1 = $loc1 ? $loc1 : array();
            $loc2 = glob($this->getDir('/lang/url-??_??.php'));
            $loc2 = $loc2 ? $loc2 : array();
            $urlFiles = array_merge($loc1, $loc2);
            $urls = array();
            foreach ($urlFiles as $urlFile) {
                require $urlFile;
                $urlFile = substr($urlFile, strrpos($urlFile, '-') + 1);
                $urlFile = substr($urlFile, 0, strpos($urlFile, '.'));
                $urls[$urlFile] = $url;
            }
            $this->url = $urls;
        }
        return $this->url;
    }

    public function getRights() {
        if (empty($this->rights) && file_exists($this->getDir('/rights'))) {
            require_once $this->getDir('/rights');
            $this->rights = $rights;
        }
        return $this->rights;
    }

    public function isSystem() {
        return $this->system;
    }

    public function isAdministrable() {
        return $this->administrable;
    }

    public function getVersion() {
        return $this->version;
    }

    public function getCompatible() {
        return $this->compatible;
    }

    public function getMetaInfo($field, $url) {
        return '';
    }

    public function install() {
        return true;
    }

    public function uninstall() {
        return true;
    }

    public function initDisplay($param = null) {
        if (Kernel::getMode() == Kernel::ADMIN) {
            if (!$this->isAdministrable() || !$this->hasRights('view admin')) {
                $this->initDisplayBlank($param);
                return;
            }
        }
        $method = false;
        $params = array();
        if (!is_string($param) || ($param == '')) {
            $this->getDisplayReflectionParamsFromURL($method, $params);
        } else {
            $this->parseDisplayReflectionParameters($method, $params, $param);
        }
        $method = 'initDisplay' . (Kernel::getMode() == Kernel::ADMIN ?
                'Admin' : 'Public') . strtoupper(substr($method, 0, 1)) .
                substr($method, 1);
        $reflectionObject = new ReflectionObject($this);
        try {
            $method = $reflectionObject->getMethod($method);
            $paramCount = $method->getNumberOfParameters();
            if (count($params) < $paramCount) {
                $paramCount = $method->getNumberOfRequiredParameters();
                if (count($params) < $paramCount) {
                    $this->displayBlank($param);
                } else {
                    $method->invokeArgs($this, $params);
                }
            } else {
                $params = count($params) > $paramCount ? array_slice($params,
                        0, $paramCount) : $params;
                $method->invokeArgs($this, $params);
            }
        } catch (Exception $e) {
            $this->initDisplayBlank($param);
        }
    }

    public function display($param = null) {
        if (Kernel::getMode() == Kernel::ADMIN) {
            if (!$this->isAdministrable() || !$this->hasRights('view admin')) {
                $this->displayBlank($param);
                return;
            }
        }
        $method = false;
        $params = array();
        if (!is_string($param) || ($param == '')) {
            $this->getDisplayReflectionParamsFromURL($method, $params);
        } else {
            $this->parseDisplayReflectionParameters($method, $params, $param);
        }
        $method = 'display' . (Kernel::getMode() == Kernel::ADMIN ? 'Admin' :
                'Public') . strtoupper(substr($method, 0, 1)) .
                substr($method, 1);
        $reflectionObject = new ReflectionObject($this);
        try {
            $method = $reflectionObject->getMethod($method);
            $paramCount = $method->getNumberOfParameters();
            if (count($params) < $paramCount) {
                $paramCount = $method->getNumberOfRequiredParameters();
                if (count($params) < $paramCount) {
                    $this->displayBlank($param);
                } else {
                    $method->invokeArgs($this, $params);
                }
            } else {
                $params = count($params) > $paramCount ? array_slice($params,
                        0, $paramCount) : $params;
                $method->invokeArgs($this, $params);
            }
        } catch (Exception $e) {
            $this->displayBlank($param);
        }
    }

    public function getHtaccess() {
        return '';
    }

    public function getRightLocalization($rightId) {
        if (!is_array($this->rightLocales)) {
            $loc1 = glob($this->getDir('/lang/rights-??.php'));
            $loc1 = $loc1 ? $loc1 : array();
            $loc2 = glob($this->getDir('/lang/rights-??_??.php'));
            $loc2 = $loc2 ? $loc2 : array();
            $localization = array_merge($loc1, $loc2);
            foreach ($localization as &$locale) {
                $locale = substr($locale, strrpos($locale, '-') + 1);
                $locale = substr($locale, 0, strpos($locale, '.'));
            }
            $localization = array_fill_keys($localization, true);
            $localization = $this->chooseLocale($localization);
            $rights = array();
            require $this->getDir("/lang/rights-$localization.php");
            $this->rightLocales = $rights;
        }
        return $this->rightLocales[$rightId];
    }

    protected function fillRecord($record, $data) {
        foreach ($record['properties'] as $key => $type) {
            if (isset($data[$key])) {
                $record['data'][$key] = $data[$key];
            }
        }
        return $record;
    }

    protected function fillRecordList($record, $data) {
        $records = array();
        foreach ($data as $key => &$value) {
            if (isset($record['properties'][$key]) && is_array($value)) {
                foreach ($value as $i => $val) {
                    $record['data'] = array();
                    foreach ($record['properties'] as $prop => $type) {
                        if (isset($data[$prop]) && is_array($data[$prop]) &&
                                isset($data[$prop][$i])) {
                            $record['data'][$prop] = $data[$prop][$i];
                        }
                    }
                    $records[] = $record;
                }
                break;
            }
        }
        return $records;
    }

    protected function initDisplayBlank($param = null) {}

    protected function displayBlank($param = null) {}

    protected function hasRights($rightId) {
        return Modules::getModule('Users')->checkRights($this->name, $rightId);
    }

    protected function getDir($path = '', $relative = false) {
        return ($relative ? '' : Config::ROOT) . "/modules/{$this->name}$path";
    }

    protected function getLink($path = '', $event = '') {
        $url = $this->extractLocalizedUrl($this->getUrls());
        $url = $url !== false ? $url : $this->name;
        if ($event == '') {
            return Config::WEB_DIR . '/' . ((Kernel::getMode() ==
                    Kernel::ADMIN) || isset($_GET['inAdmin']) ||
                    isset($_POST['inAdmin']) ? 'admin/' : '') . "$url/$path";
        } else {
            return Config::WEB_DIR . '/event/' . (Kernel::getMode() ==
                    Kernel::ADMIN ? 'admin/' : '') . "$event/$url/$url/$path";
        }
    }

    protected function getForm($event, $source = false, $target = false,
                $onsubmit = false) {
        $code = '<form method="post" enctype="multipart/form-data" action="' .
                $this->getLink() . '"' . ($onsubmit ?
                " onsubmit=\"return $onsubmit(this);\"" : '') . '>' . "\n" .
                '<input type="hidden" name="mode" value="event" />' . "\n" .
                '<input type="hidden" name="event" value="' . $event . '" />' .
                "\n";
        if (Kernel::getMode() == Kernel::ADMIN) {
            $code .= '<input type="hidden" name="inAdmin" value="true" />' .
                    "\n";
        }
        if ($source) {
            $url = $this->extractLocalizedUrl($this->getUrls());
            $url = $url !== false ? $url : $this->name;
            $code .= '<input type="hidden" name="source" value="' . $url .
                    '" />' . "\n";
        }
        if ($target !== false) {
            $mod = Modules::getModule($target);
            if ($mod != null) {
                $urls = $mod->getUrls();
                $url = $this->extractLocalizedUrl($urls);
                $target = $url !== false ? $url : $target;
            }
            $code .= '<input type="hidden" name="target" value="' . $target .
                    '" />' . "\n";
        }
        return $code;
    }

    protected function lang($key, &$params = array()) {
        $template = isset($this->langArray[$key]) ? $this->langArray[$key] :
                Modules::getModule('View')->getLang($key);
        if ($template == '') {
            return $key;
        }
        if (strpos($template, '{') === false) {
            return $template;
        }
        $parts = &$this->parseLangTemplate($template);
        $this->replaceConditions($parts, $params);
        $this->replaceVars($parts, $params);
        $template = implode('', $parts);
        return $template;
    }
    
    protected function template($name, &$params = array()) {
        $params = array_merge($this->langArray, $params);
        Modules::getModule('View')->displayTemplate($this->getName(), $name,
                $params);
    }

    protected function templateExists($name) {
        $path = null;
        return Modules::getModule('View')->checkTemplate($this->getName(),
                $name, $path);
    }

    protected function templateOverrideChain($names, &$params = array()) {
        $params = array_merge($this->langArray, $params);
        $use = null;
        $path = null;
        foreach ($names as $cur) {
            if ($use === null && Modules::getModule('View')->
                    checkOverrideTemplate($this->getName(), $cur, $path)) {
                $use = $cur;
            }
        }
        foreach ($names as $cur) {
            if ($use === null && Modules::getModule('View')->
                    checkThemeTemplate($this->getName(), $cur, $path)) {
                $use = $cur;
            }
        }
        foreach ($names as $cur) {
            if ($use === null && Modules::getModule('View')->
                    checkDefaultTemplate($this->getName(), $cur, $path)) {
                $use = $cur;
            }
        }
        Modules::getModule('View')->displayTemplate($this->getName(), $use,
                $params);
    }

    protected function js($path, $relative = true) {
        if ($relative) {
            $path = Config::WEB_DIR . $this->getDir($path, true);
        }
        Modules::getModule('View')->addJS($path);
    }

    protected function css($path, $relative = true) {
        if ($relative) {
            $path = Config::WEB_DIR . $this->getDir($path, true);
        }
        Modules::getModule('View')->addCSS($path);
    }

    protected function displayForm(&$form) {
        $this->prepareFormData($form);
        $params = array('form' => &$form);
        $this->template('form', $params);
    }

    protected function &createAddForm($event, $tableSpecs) {
        $form = array('start' => $event, 'legend' => "{$event}Form",
                'submit' => 'create', 'items' => array());
        foreach ($tableSpecs as $column => $type) {
            if (strpos($type, '+') !== false) {
                continue;
            }
            $form['items'][] = array('name' => $column, 'label' => $column);
        }
        return $form;
    }

    protected function &createEditForm($event, $tableSpecs, $data = null) {
        if ($data === null) {
            $form = &$this->createEditForm($event, $tableSpecs['properties'],
                    $tableSpecs['data']);
            return $form;
        }
        $form = array('start' => $event, 'legend' => "{$event}Form",
                'submit' => 'save', 'items' => array());
        foreach ($tableSpecs as $column => $type) {
            $item = array('name' => $column, 'label' => $column, 'value' =>
                    $data[$column]);
            if (strpos($type, '+') !== false) {
                $item['type'] = 'hidden';
            }
            $form['items'][] = $item;
        }
        return $form;
    }

    protected function addMsg($message, $level) {
        Modules::getModule('View')->addMessage($this->lang($message), $level);
    }

    protected function chooseLocale(&$from) {
        if (!is_array($from) || (count($from) == 0)) {
            return null;
        }
        $keys = array_keys($from);
        if (is_numeric($keys[0])) {
            $keys = $from;
        }
        if (array_search(Kernel::getLanguage(), $keys) !== false) {
            return Kernel::getLanguage();
        }
        if (strlen(Kernel::getLanguage()) > 2) {
            if (array_search(substr(Kernel::getLanguage(), 0, 2), $keys) !==
                    false) {
                return substr(Kernel::getLanguage(), 0, 2);
            }
        }
        if (Kernel::getLanguage() != Config::DEFAULT_LANG) {
            if (array_search(Config::DEFAULT_LANG, $keys) !== false) {
                return Config::DEFAULT_LANG;
            }
            if (strlen(Config::DEFAULT_LANG) > 2) {
                if (array_search(substr(Config::DEFAULT_LANG, 0, 2), $keys) !==
                        false) {
                    return substr(Config::DEFAULT_LANG, 0, 2);
                }
            }
        }
        $keys2 = array_keys($keys);
        if (count($keys2) > 0) {
            return $keys[$keys2[0]];
        }
        return false;
    }

    protected function getLocale(&$from) {
        if (is_array($from)) {
            return $from[$this->chooseLocale($from)];
        }
        return null;
    }

    protected function prepareFormData(&$form) {
        foreach ($form['items'] as &$item) {
            if (isset($item['label'])) {
                $item['label'] = $this->lang($item['label']);
            }
            if (array_search($item['name'], array('source', 'event', 'inAdmin',
                    'target', 'mode')) !== false) {
                trigger_error("Field {$item['name']} is reserved, you " .
                        "shouldn't use it", E_USER_WARNING);
            }
        }
        if (isset($form['submit'])) {
            $form['submit'] = $this->lang($form['submit']);
        }
        if (isset($form['legend'])) {
            $form['legend'] = $this->lang($form['legend']);
        }
        if (isset($form['submitError'])) {
            $form['submitError'] = $this->lang($form['submitError']);
        }
        if (strpos($form['start'], '<') === false) {
            $form['start'] = $this->getForm($form['start'], true,
                    $this->getName());
        }
        if (isset($form['submitNote'])) {
            $form['submitNote'] = $this->lang($form['submitNote']);
        }
    }

    protected function prepareMenu(&$menu) {
        foreach ($menu as &$item) {
            $item['label'] = $this->lang($item['label']);
            $item['title'] = isset($item['title']) ?
                    $this->lang($item['title']) : '';
            $item['href'] = is_array($item['href']) ?
                    $this->getLink($item['href'][0], $item['href'][1]) :
                    $this->getLink($item['href']);
        }
    }
    
    protected function loadLang($localization) {
        $lang = array();
        require_once $this->getDir("/lang/$localization.php");
        $this->langArray = $lang;
    }

    /**
     * Parses information obtained from parameter for the display function to
     * get name of the method to be called and it's parameters.
     *
     * @param String $method Name of the method to be called.
     * @param array $params Array containing parameters for the method.
     * @param mixed $param Display parameter to be parsed
     */
    private function parseDisplayReflectionParameters(&$method, &$params,
            $param) {
        if (strpos($param, '-') !== false) {
            list($method, $params) = explode('-', $param, 2);
            $params = explode('-', $params);
        } else {
            $method = $param;
        }
    }

    /**
     * Tries to extract the vital information about methods to be called and
     * it's parameters from the URL.
     *
     * @param string $method Method name.
     * @param array $params Method parameters.
     */
    private function getDisplayReflectionParamsFromURL(&$method, &$params) {
        $url = Modules::getModule('View')->getUrl();
        if (count($url) > 0) {
            if (Modules::resolveModuleUrl($url[0]) == $this->getName()) {
                if (count($url) > 2) {
                    $method = $url[1];
                    $params = array_slice($url, 2);
                } else {
                    $method = count($url) > 1 ? $url[1] : 'index';
                }
            } else {
                $url = Modules::getModule('View')->getUrl(null);
                if ((count($url) > 0) && (Modules::resolveModuleUrl($url[0]) ==
                        $this->getName())) {
                    if (count($url) > 2) {
                        $method = $url[1];
                        $params = array_slice($url, 2);
                    } else {
                        $method = count($url) > 1 ? $url[1] : 'index';
                    }
                }
            }
        } else {
            $method = 'blank';
        }
        if (strpos($method, '-') !== false) {
            $method = explode('-', $method);
            $str = $method[0];
            $count = count($method);
            for ($i = 1; $i < $count; $i++) {
                $str .= strtoupper(substr($method[$i], 0, 1)) .
                        substr($method[$i], 1);
            }
            $method = $str;
        }
    }

    /**
     * Extracts one URL for current language (if possible) from urls array.
     *
     * @param Array $urls List of URL for paricular languages.
     * @return String Localized url. If no URL is found, returns false.
     */
    private function extractLocalizedUrl($urls) {
        if (!is_array($urls)) {
            return false;
        }
        return $this->getLocale($urls);
    }

    private function replaceConditions(&$parts, &$params) {
        $result = array();
        $include = true;
        foreach ($parts as &$part) {
            if (is_array($part)) {
                if (substr($part[0], 0, 2) == 'if') {
                    $include = isset($params[substr($part[0], strpos($part[0],
                            '$') + 1)]) && $params[substr($part[0],
                            strpos($part[0], '$') + 1)];
                } elseif ($part[0] == 'else') {
                    $include = !$include;
                } elseif ($part[0] == '/if') {
                    $include = true;
                } elseif ($include) {
                    $result[] = $part;
                }
            } elseif ($include) {
                $result[] = $part;
            }
        }
        $parts = $result;
    }

    private function replaceVars(&$parts, &$params) {
        foreach ($parts as &$part) {
            if (is_array($part)) {
                if (substr($part[0], 0, 1) == '$') {
                    if (isset($params[substr($part[0], 1)])) {
                        $part = $params[substr($part[0], 1)];
                    } else {
                        $part = '';
                    }
                }
            }
        }
    }

    private function &parseLangTemplate(&$template) {
        $parts0 = explode('{', $template);
        $parts = array();
        foreach ($parts0 as $part) {
            if (strpos($part, '}') === false) {
                $parts[] = $part;
            } else {
                $part = explode('}', $part, 2);
                $parts[] = array($part[0]);
                $parts[] = $part[1];
            }
        }
        return $parts;
    }

    /**
     * Formats output from conditions.
     *
     * @param String $conditions Conditions string.
     * @param int $parameter Condition parameter.
     */
    private function formatCondition($conditions, $parameter) {
        $options = split('|', $conditions);
        $output = '';
        foreach ($options as $option) {
            list($condition, $format) = split('$', $option, 2);
            if (is_numeric($condition) &&
                    ($conditions == (int) $parameter)) {
                $output = $format;
                break;
            } elseif ((substr($condition, 0, 1) == '!') &&
                    (substr($condition, 0) == $parameter)) {
                $output = $format;
                break;
            } elseif ((substr($condition, -1) == '>') &&
                    (substr($condition, 0, -1) > $parameter)) {
                $output = $format;
                break;
            } elseif ((substr($condition, -1) == '<') &&
                    (substr($condition, 0, -1) < $parameter)) {
                $output = $format;
            }
        }
        return $this->formatNumber($output, $parameter);
    }

    /**
     * Formats number according to the format string.
     *
     * @param String $format Format string of the output.
     * @param String $number Number to be formatted.
     * @return String Formatted number.
     */
    private function formatNumber($format, $number) {
        $leght = strlen($number);
        $hashpos = strrpos($format, '#');
        while (($hashpos !== false) && ($leght > 0)) {
            while (($hashpos > 0) &&
                    (substr($format, $hashpos - 1, 1) == '\\')) {
                $hashpos = strrpos($format, '#', $hashpos - 1);
            }
            $last = substr($number, $leght - 1);
            $format = substr($format, 0, $hashpos - 2) . $last .
                    substr($format, $hashpos + 1);
            $number = substr($number, $leght - 2);
            $leght--;
        }
        return $format;
    }
}
?>
